/**
 *  Library imports.
 */
var fs = require("fs"),
    ejs = require("ejs"),
    http = require('http'),
    path = require("path"),
    mysql = require('mysql'),
    sqlite3 = require("sqlite3"),
    url = require("url"),
    generic_pool = require('generic-pool');

/**
 *  Prototype extensions.
 */
String.prototype.startsWith = function(s) {
  return this.length >= s.length && this.substring(0, s.length) == s;
};
String.prototype.endsWith = function(s) {
  return this.length >= s.length && this.substring(this.length - s.length) == s;
};
String.prototype.contains = function(s) {
  return this.length >= s.length && this.indexOf(s) >= 0;
};
String.prototype.trim = function() {
  return this.replace(/^\s+/, '').replace(/\s+$/, '');
}

/**
 *  Globals.
 */
var port = 8080;
var host = '0.0.0.0';

var pool = generic_pool.Pool({
  name: 'mysql',
  max: 10,
  create: function(callback) {
    var db = mysql.createConnection({
      hostname: 'localhost',
      user: 'root',
      password: '',
      database: 'clbc'
    });
    db.connect(function(err, server) {
      db.query('SET character_set_client=utf8');
      db.query('SET character_set_connection=utf8');
      db.query('SET character_set_results=utf8');
      callback(err, db);
    });
  },
  destroy: function(db) {
    db.end();
  }
});

var resourcePath = process.cwd() + '/resources';
var commands = {};
var queryCache = {};
var templates = {
  app_name_in_title: "CLBC Explorer",
  app_name_in_path: "CLBC Explorer",
  app_name_in_header: "Chinese Learners' Blog Corpus Explorer",
  greetings:
    "<p style='font-size: 135%; width: 35em; color: #a00'>" +
    "Welcome to this Chinese Learners' Blog Corpus Explorer, populated with blog posts " +
    "written by students attending courses XYZ from 20XX to 20XX, taught by ABC at University of PQR." +
    "</p>",
  footer:
    '<div id="footer">' +
      '<a href="http://code.google.com/p/clbc-explorer/" target="_blank">project site</a>' +
    '</div>',
  
  css_prolog: '',
  css_epilog: '',
  js_prolog: '',
  js_epilog: '',
  path_prefix: ''
};

function loadTemplates() {
  ejs.open = '{{';
  ejs.close = '}}';
  
  var templatePath = process.cwd() + '/templates';
  var loadAndOverride = function(fileName, field) {
    var filePath = path.join(templatePath, fileName);
    path.exists(filePath, function(exists) {
      if (exists) {
        fs.readFile(filePath, "binary", function(err, file) {
          if (!err) {
            templates[field] = file;
          }
        });
      }
    });
  };
  
  loadAndOverride("app_name_in_title.txt", "app_name_in_title");
  loadAndOverride("app_name_in_path.txt", "app_name_in_path");
  loadAndOverride("app_name_in_header.txt", "app_name_in_header");
  loadAndOverride("greetings.html", "greetings");
  loadAndOverride("footer.html", "footer");
  
  loadAndOverride("css_prolog.html", "css_prolog");
  loadAndOverride("css_epilog.html", "css_epilog");
  loadAndOverride("js_prolog.html", "js_prolog");
  loadAndOverride("js_epilog.html", "js_epilog");
  
  loadAndOverride("path_prefix.html", "path_prefix");
}

function serveStaticResource(request, response, urlPath) {
  var filePath = path.join(resourcePath, urlPath);
  path.exists(filePath, function(exists) {
    if (!exists) {
      response.writeHead(404, { "Content-Type": "text/plain" });
      response.write("404 Not Found\n");
      response.end();
      return;
    }
  	if (fs.statSync(filePath).isDirectory()) {
  	  filePath += '/index.html';
    }
    
    fs.readFile(filePath, "binary", function(err, file) {
      if (err) {
        response.writeHead(500, { "Content-Type": "text/plain" });
        response.write(err + "\n");
        response.end();
        return;
      }
      
      //var encoding = "UTF-8";
      var contentType = "text/plain; charset=UTF-8";
      if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
        contentType = "text/html; charset=UTF-8";
        file = ejs.render(file, templates);
      } else if (filePath.endsWith(".js")) {
        contentType = "text/javascript; charset=UTF-8";
      } else if (filePath.endsWith(".json")) {
        contentType = "application/json; charset=UTF-8";
      } else if (filePath.endsWith(".css")) {
        contentType = "text/css; charset=UTF-8";
      } else {
        //encoding = "binary";
        if (filePath.endsWith(".png")) {
          contentType = "image/png";
        } else {
          contentType = "application/binary";
        }
      }
      response.writeHead(200, { "Content-Type": contentType });
      response.write(file /*, encoding */);
      response.end();
    });
  });
}

function serveJsonp(request, response, parsedUrl, json) {
  var callback = parsedUrl.query.callback;
  var contentType = ((callback) ? "text/javascript" : "application/json") + '; charset=UTF-8';
  
  response.writeHead(200, { "Content-Type": contentType });
  if (callback) {
    response.write(callback.replace(/\W/g, ''));
    response.write('(');
  }
  response.write(JSON.stringify(json, null, 1));
  if (callback) {
    response.write(');');
  }
  response.end();
}

function makeWhereClause(subClauses) {
  var s = [];
  for (var i = 0; i < subClauses.length; i++) {
    var subClause = subClauses[i];
    if (subClause.length > 0) {
      s.push(subClause);
    }
  }
  return s.length == 0 ? '' : ('WHERE ' + s.join(' AND '));
}

function makeIntegerWhere(parsedUrl, paramName, conditionPrefix, defaultCase) {
  defaultCase = defaultCase || '';
  if (paramName in parsedUrl.query) {
    try {
      var values = parsedUrl.query[paramName].trim().split(';');
      if (values.length > 0) {
        var subclauses = [];
        for (var i = 0; i < values.length; i++) {
          var n = parseInt(values[i]);
          if (typeof n == 'number') {
            subclauses.push(conditionPrefix + n);
          } else {
            return defaultCase;
          }
        }

        return subclauses.length == 1 ? subclauses[0] : ('(' + subclauses.join(' OR ') + ')');
      }
    } catch (e) {
      // Ignore
    }
  }
  return defaultCase;
}

function makeStringWhere(parsedUrl, paramName, conditionPrefix, defaultCase) {
  defaultCase = defaultCase || '';
  if (paramName in parsedUrl.query) {
    var values = parsedUrl.query[paramName].trim().split(';');
    if (values.length > 0) {
      var subclauses = [];
      for (var i = 0; i < values.length; i++) {
        subclauses.push(conditionPrefix + '"' + values[i] + '"');
      }

      return subclauses.length == 1 ? subclauses[0] : ('(' + subclauses.join(' OR ') + ')');
    }
  }
  return defaultCase || '';
}

function subst(query, params) {
  var segments = [];
  var start = 0;
  for (var i = 0; i < params.length; i++) {
    var param = params[i];
    var question = query.indexOf('?', start);
    if (question < 0) {
      throw new Error('Query ' + q + ' has fewer ? slots than provided parameters');
    }
    segments.push(query.substring(start, question));
    start = question + 1;
    if (typeof param == 'string') {
      segments.push('"' + param + '"');
    } else {
      segments.push(param.toString());
    }
  }
  if (start < query.length) {
    segments.push(query.substring(start));
  }
  return segments.join('');
}

commands['get-corpus-stats'] = function(db, request, response, parsedUrl) {
  var result = { tables: {} };
  var countTable = function(table, field) {
    db.query('SELECT COUNT(*) AS c FROM ' + table, function (err, records, columns) {
      if (err) {
        console.log(err);
      }
      for (var r = 0; r < records.length; r++) {
        result.tables[field] = records[r].c;
      }
    });
  };
  countTable('bloggers', 'bloggers');
  countTable('blogs', 'blogs');
  countTable('posts', 'posts');
  countTable('sentences', 'sentences');
  countTable('words', 'words');
  db.query('SELECT MIN(date) AS earliest, MAX(date) AS latest FROM posts WHERE date > 0', function(err, records, columns) {
    for (var r = 0; r < records.length; r++) {
      result.earliest = records[r].earliest;
      result.latest = records[r].latest;
    }
    serveJsonp(request, response, parsedUrl, result);
  });
};

commands['get-frequent-words'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var lengthClause = makeIntegerWhere(parsedUrl, 'word_length', 'word_length = ');
  var tagClause = makeStringWhere(parsedUrl, 'tag', 'pos_tag = ', 'pos_tag <> "PU"');
  var inSentenceTokenIndexClause = makeIntegerWhere(parsedUrl, 'in_sentence_token_index', 'in_sentence_token_index = ');

  var q = [
    'SELECT word, pos_tag, COUNT(*) AS c FROM words',
    makeWhereClause([
      tagClause,
      lengthClause,
      inSentenceTokenIndexClause
    ]),
    'GROUP BY word, pos_tag',
    'ORDER BY COUNT(*) DESC',
    limitClause
  ].join(' ');
  
  if (q in queryCache) {
    serveJsonp(request, response, parsedUrl, { results: queryCache[q] });
    return;
  }

  var results = [];
  db.query(q, function(err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    if (records) {
      for (var r = 0; r < records.length; r++) {
        var record = records[r];
        results.push({ w: record.word, t: record.pos_tag, c: record.c });
      }
    }
    queryCache[q] = results;
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

commands['get-frequent-word-lengths'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var tagClause = makeStringWhere(parsedUrl, 'tag', 'pos_tag = ', 'pos_tag <> "PU"');
  var inSentenceTokenIndexClause = makeIntegerWhere(parsedUrl, 'in_sentence_token_index', 'in_sentence_token_index = ');

  var q = [
    'SELECT word_length AS l, COUNT(*) AS c FROM words',
    makeWhereClause([
      tagClause,
      inSentenceTokenIndexClause
    ]),
    'GROUP BY word_length',
    'ORDER BY COUNT(*) DESC',
    limitClause
  ].join(' ');
  
  if (q in queryCache) {
    serveJsonp(request, response, parsedUrl, { results: queryCache[q] });
    return;
  }

  var results = [];
  db.query(q, function(err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    if (records) {
      for (var r = 0; r < records.length; r++) {
        var record = records[r];
        results.push({ l: record.l, c: record.c });
      }
    }
    queryCache[q] = results;
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

commands['get-frequent-tags'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var lengthClause = makeIntegerWhere(parsedUrl, 'word_length', 'word_length = ');
  var inSentenceTokenIndexClause = makeIntegerWhere(parsedUrl, 'in_sentence_token_index', 'in_sentence_token_index = ');
  var q = [
    'SELECT pos_tag, COUNT(*) AS c FROM words',
    makeWhereClause([
      lengthClause,
      inSentenceTokenIndexClause
    ]),
    'GROUP BY pos_tag',
    'ORDER BY COUNT(*) DESC',
    limitClause
  ].join(' ');
  
  if (q in queryCache) {
    serveJsonp(request, response, parsedUrl, { results: queryCache[q] });
    return;
  }

  var results = [];
  db.query(q, function (err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    if (records) {
      for (var r = 0; r < records.length; r++) {
        var record = records[r];
        results.push({ t: record.pos_tag, c: record.c });
      }
    }
    queryCache[q] = results;
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

commands['get-following-words'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var word = parsedUrl.query.word;
  var wordTagClause = makeStringWhere(parsedUrl, 'word_tag', 'pos_tag_before = ');
  
  var q = [
    'SELECT word_after AS w, pos_tag_after AS t, COUNT(word_after_id) AS c FROM followings',
    makeWhereClause([ 'word_before = ?', wordTagClause ]),
    'GROUP BY word_after, pos_tag_after ORDER BY COUNT(word_after_id) DESC',
    limitClause
  ].join(' ');
  q = subst(q, [ word ]);

  if (q in queryCache) {
    serveJsonp(request, response, parsedUrl, { results: queryCache[q] });
    return;
  }

  var results = [];
  db.query(q, function(err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    for (var r = 0; r < records.length; r++) {
      var record = records[r];
      results.push({ w: record.w, t: record.t, c: record.c });
    }
    queryCache[q] = results;
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

commands['get-preceding-words'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var word = parsedUrl.query.word;
  var wordTagClause = makeStringWhere(parsedUrl, 'word_tag', 'pos_tag_after = ');
  
  var q = [
    'SELECT word_before AS w, pos_tag_before AS t, COUNT(word_before_id) AS c FROM followings',
    makeWhereClause([ 'word_after = ?', wordTagClause ]),
    'GROUP BY word_before, pos_tag_before ORDER BY COUNT(word_before_id) DESC',
    limitClause
  ].join(' ');
  q = subst(q, [ word ]);

  if (q in queryCache) {
    serveJsonp(request, response, parsedUrl, { results: queryCache[q] });
    return;
  }

  var results = [];
  db.query(q, function(err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    for (var r = 0; r < records.length; r++) {
      var record = records[r];
      results.push({ w: record.w, t: record.t, c: record.c });
    }
    queryCache[q] = results;
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

commands['get-sample-sentences-containing-word'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var offsetClause = makeIntegerWhere(parsedUrl, 'offset', 'OFFSET ');
  
  var word = parsedUrl.query.word;
  var wordTagClause = makeStringWhere(parsedUrl, 'word_tag', 'pos_tag = ');
  
  var q = [
    'SELECT sentences.id AS id, sentences.segmented_text AS segmented_text, sentences.tree AS tree, posts.url AS url',
    'FROM (SELECT sentence_id FROM words ' + makeWhereClause([ 'words.word = ?', wordTagClause ]) + ') AS words1',
    'JOIN sentences ON words1.sentence_id = sentences.id',
    'JOIN posts ON posts.id = sentences.post_id',
    limitClause,
    offsetClause
  ].join(' ');
  q = subst(q, [ word ]);

  var results = [];
  db.query(q, function(err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    for (var r = 0; r < records.length; r++) {
      var record = records[r];
      results.push({ id: record.id, segmented_text: record.segmented_text, tree: record.tree, url: record.url });
    }
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

commands['get-sample-sentences-containing-two-words'] = function(db, request, response, parsedUrl) {
  var limitClause = makeIntegerWhere(parsedUrl, 'limit', 'LIMIT ', 'LIMIT 100');
  var offsetClause = makeIntegerWhere(parsedUrl, 'offset', 'OFFSET ');
  
  var word1 = parsedUrl.query.word1;
  var wordTagClause1 = makeStringWhere(parsedUrl, 'word_tag1', 'pos_tag_before = ');
  
  var word2 = parsedUrl.query.word2;
  var wordTagClause2 = makeStringWhere(parsedUrl, 'word_tag2', 'pos_tag_after = ');
  
  var q = [
    'SELECT sentences.id AS id, sentences.segmented_text AS segmented_text, sentences.tree AS tree, posts.url AS url',
    'FROM ',
      '(SELECT sentence_id FROM followings',
      makeWhereClause([ 'word_before = ?', wordTagClause1, 'word_after = ?', wordTagClause2 ]),
      ') AS followings',
    'JOIN sentences ON followings.sentence_id = sentences.id',
    'JOIN posts ON posts.id = sentences.post_id',
    limitClause,
    offsetClause
  ].join(' ');
  q = subst(q, [ word1, word2 ]);

  var results = [];
  db.query(q, function(err, records) {
    if (err) {
      serveJsonp(request, response, parsedUrl, { error: err });
      return;
    }
    for (var r = 0; r < records.length; r++) {
      var record = records[r];
      results.push({ id: record.id, segmented_text: record.segmented_text, tree: record.tree, url: record.url });
    }
    serveJsonp(request, response, parsedUrl, { results: results });
  });
};

loadTemplates();
http.createServer(function (request, response) {
    try {
      var parsedUrl = url.parse(request.url, true, true);
      var urlPath = parsedUrl.pathname;
      urlPath = urlPath.replace(/^\/\/+/, '/');
      console.log(new Date().toString() + ': ' + urlPath);
      if (urlPath.startsWith('/-/')) {
        var command = urlPath.substring(3);
        if (command in commands) {
          pool.acquire(function(err, db) {
            if (err) {
              return res.end("CONNECTION error: " + err);
            } else {
              commands[command](db, request, response, parsedUrl);
              pool.release(db);
            }
          });
        } else {
          response.writeHead(500, { "Content-Type": "text/plain" });
          response.write("Invalid command: " + command);
          response.end();
        }
      } else if (!urlPath.startsWith('/..')) {
        serveStaticResource(request, response, urlPath);
      } else {
        response.writeHead(500, { "Content-Type": "text/plain" });
        response.write("Illegal attempt to access files outside webapp: " + urlPath);
        response.end();
      }
    } catch (e) {
      console.log(e);
    }
  })
  .listen(port, host);

console.log(
  '\nServer running at http://' + host + ':' + port + '/' +
  '\nPress Ctrl-C to stop this server.'
);
